module pflanza.polygen;

import objects.triangle;
import pflanza.lsystem;
import pflanza.tesselator;
import std.boxer;
import cmath = std.c.math;
import std.math;
import std.random;
import std.stream;
import utils.mover3;
import utils.mt19937ar;
import utils.vector2;
import utils.vector3;

float toRad(float degree) {
    return degree*PI/180.0f;
}

public class Polygen {
    private {
        LSystemParser mParser;
        Mover3f mInitialState;
        Triangle[] mFaces;
        TriangleTex[] mFaceTex;
        int[] mShaders;

        Vector3f[] mCurrentPoly;

        Tesselator mTess;

        struct TurtleState {
            Mover3f mover;
            float width;
            int materialIdx;
            float tex_y = 0;
        }

        struct TurtleData {
            TurtleState state_last, state_cur;
        }
    }

    public this(LSystemParser parser) {
        //assuming mt is already initialized
        mTess = new Tesselator();

        mParser = parser;
        mParser.registerCommand("R",1,&action);
        mParser.registerCommand("T",1,&action);
        mParser.registerCommand("P",1,&action);
        mParser.registerCommand("F",1,&action);
        mParser.registerCommand("f",1,&action);
        mParser.registerCommand("G",1,&action);
        mParser.registerCommand("Gs",1,&action);
        mParser.registerCommand("up",0,&action);
        mParser.registerCommand("M",1,&action);
        mParser.registerCommand("POLYOPEN",0,&action);
        mParser.registerCommand("POLYCLOSE",0,&action);
        mParser.registerCommand("POLYPOINT",0,&action);
        mParser.registerFunction("rand",2,&func);
        mParser.registerFunction("sqrt",1,&func);
    }

    public void execute(Mover3f initialState, float initWidth) {
        //mParser.reset();
        mFaces.length = 0;
        mFaceTex.length = 0;
        mShaders.length = 0;
        mInitialState = initialState;
        TurtleState ts;
        ts.mover = mInitialState;
        ts.width = -1;
        ts.materialIdx = 0;
        TurtleData td;
        td.state_last = ts;
        td.state_cur = ts;
        mParser.vars.userdata_state = box(td);

        mParser.start();
    }

    public void save(char[] filename) {
        auto rawFile = new File(filename, FileMode.OutNew);
        auto matFile = new File(filename ~ ".mat", FileMode.OutNew);

        rawFile.writeBlock(mFaces.ptr,mFaces.length*Triangle.sizeof);
        rawFile.writeBlock(mFaceTex.ptr,mFaceTex.length*TriangleTex.sizeof);
        matFile.writeBlock(mShaders.ptr,mShaders.length*int.sizeof);
    }

    private void action(char[] name, float[] params) {
        //std.stdio.writefln("%s%s",name,params);
        TurtleData td = unbox!(TurtleData)(mParser.vars.userdata_state);
        switch (name) {
            case "R":
                //roll
                td.state_cur.mover.rotateRelative(Vector3f(0,0,toRad(params[0])));
                break;
            case "T":
                //turn, i.e. yaw
                td.state_cur.mover.rotateRelative(Vector3f(0,toRad(params[0]),0));
                break;
            case "P":
                //pitch
                td.state_cur.mover.rotateRelative(Vector3f(toRad(params[0]),0,0));
                break;
            case "F":
                //move forward and draw
                td.state_cur.mover.moveRelative(Vector3f(0,0,params[0]));
                //make the stem straight
                if (td.state_last.width < 0)
                    td.state_last.width = td.state_cur.width;
                addLine(td.state_last,td.state_cur);
                td.state_last = td.state_cur;
                break;
            case "f":
                //move forward, no drawing
                td.state_cur.mover.moveRelative(Vector3f(0,0,params[0]));
                break;
            case "G":
                td.state_cur.width += params[0];
                //std.stdio.writefln("G = %s",td.state_cur.width);
                break;
            case "Gs":
                td.state_cur.width = params[0];
                //std.stdio.writefln("G = %s",td.state_cur.width);
                break;
            case "up":
                Vector3f l = (mInitialState.dir | td.state_cur.mover.dir).normal();
                td.state_cur.mover.up = td.state_cur.mover.dir | l;
                break;
            case "M":
                td.state_cur.materialIdx = cast(int)params[0];
                break;
            case "POLYOPEN":
                polyStart();
                break;
            case "POLYCLOSE":
                polyFinish(td.state_cur.materialIdx);
                break;
            case "POLYPOINT":
                polyAddPoint(td.state_cur.mover.pos);
                break;
            default:
                assert(false);
        }
        mParser.vars.userdata_state = box(td);
    }

    public float func(char[] name, float[] params) {
        switch (name) {
            case "rand":
                float r = genrand_real1();
                return params[0] + r*(params[1]-params[0]);
                break;
            case "sqrt":
                return sqrt(params[0]);
                break;
            default:
                assert(false);
        }
    }

    private float b2(float u, int n) {
        switch (n) {
            case 0: return (1-u)*(1-u);
            case 1: return 2*u*(1-u);
            case 2: return u*u;
        }
    }

    private void addLine(TurtleState state1, inout TurtleState state2) {
        const nsubdiv_rot = 15;    //number of subdivisions in circle
        const nsubdiv_len = 10;     //number of subdivisions on current line

        float circ = PI*(state1.width+state2.width)/2;
        float len = (state2.mover.pos-state1.mover.pos).length();

        Vector3f nrotplane;
        if (state1.mover.dir == state2.mover.dir) {
            nrotplane = state1.mover.up;
        } else {
            nrotplane = (state2.mover.dir | state1.mover.dir).normal();
        }

        //distance from plane (state1.mover.dir, state2.mover.pos) to point state1.mover.pos
        float d = state1.mover.dir*state2.mover.pos-state1.mover.dir*state1.mover.pos;
        Vector3f p2 = state1.mover.pos+state1.mover.dir*d;
        state2.mover.pos = state2.mover.pos+(p2-state2.mover.pos)*0.25;
        p2 = state1.mover.pos+(p2-state1.mover.pos)*0.25;

        //interpolate a point between state1 and state2 (t in [0,1])
        Mover3f pointBetween(float t) {
            Mover3f ret;
            /*Vector3f diff_pos = (state2.mover.pos - state1.mover.pos);
            Vector3f diff_dir = (state2.mover.dir - state1.mover.dir);
            ret.pos = state1.mover.pos + t*diff_pos;
            ret.dir = state1.mover.dir + t*diff_dir;*/

            ret.pos = state1.mover.pos*b2(t,0)+p2*b2(t,1)+state2.mover.pos*b2(t,2);
            ret.dir = state1.mover.dir*(1-t)+state2.mover.dir*t;
            ret.up = nrotplane;   //no rotation
            return ret;
        }

        Vector3f[] lastRow = new Vector3f[nsubdiv_rot];

        //get a point on the circle around cur with normal cur.dir
        Vector3f pointOnCircle(Mover3f cur, int iDiv, float t) {
            float width = state1.width+(state2.width-state1.width)*t;
            float alpha = iDiv*(2*PI)/nsubdiv_rot;
            return cur.pos + cur.up*sin(alpha)*width/2 + cur.side()*cos(alpha)*width/2;
        }

        Vector2f tex(float t, int i) {
            Vector2f ret;
            ret.x = cast(float)i/nsubdiv_rot;
            ret.y = state1.tex_y+t*(len/circ);
            return ret;
        }

        for (int i = 0; i < nsubdiv_rot; i++) {
            lastRow[i] = pointOnCircle(pointBetween(0),i,0);
        }

        Triangle[] faces = new Triangle[(nsubdiv_len-1)*nsubdiv_rot*2];
        TriangleTex[] faceTex = new TriangleTex[faces.length];
        int[] shaders = new int[faces.length];

        int idx = 0;
        float tOld = 0;
        for (int n = 1; n < nsubdiv_len; n++) {
            float t = cast(float)(n)/(nsubdiv_len-1);
            Mover3f cur = pointBetween(t);
            Vector3f p;
            Vector3f pprev = pointOnCircle(cur,0,t);
            for (int i = 1; i < nsubdiv_rot+1; i++) {
                p = pointOnCircle(cur,i,t);
                faceTex[idx] = TriangleTex([tex(t,i),tex(tOld,i-1),tex(t,i-1)]);
                shaders[idx] = 0;
                faces[idx++] = Triangle([p,lastRow[i-1],pprev]);
                faceTex[idx] = TriangleTex([tex(t,i),tex(tOld,i),tex(tOld,i-1)]);
                shaders[idx] = 0;
                faces[idx++] = Triangle([p,lastRow[i % nsubdiv_rot],lastRow[i-1]]);
                if (i>1) lastRow[i-1] = pprev;
                pprev = p;
            }
            lastRow[0] = p;
            tOld = t;
        }

        state2.tex_y = tex(1,0).y;

        //mDebugFile.writeBlock(&state1.mover.pos,Vector3f.sizeof);
        //mDebugFile.writeBlock(&state2.mover.pos,Vector3f.sizeof);
        mFaces ~= faces;
        mFaceTex ~= faceTex;
        mShaders ~= shaders;
    }

    private void polyStart() {
        mCurrentPoly.length = 0;
    }

    private void polyAddPoint(Vector3f point) {
        mCurrentPoly ~= point;
    }

    private void polyFinish(int materialIdx) {
        const EPS = 1e-4f;

        Triangle[] tris, trisInv;
        int[] shaders;
        //create "2-sided" triangles
        tris = mTess.tesselate(mCurrentPoly);
        trisInv.length = tris.length;
        shaders.length = tris.length;
        for (int i = 0; i < tris.length; i++) {
            //doesn't look that good with zero width
            auto n = tris[i].normal();
            trisInv[i] = Triangle([tris[i].v[0]-n*EPS,tris[i].v[2]-n*EPS,tris[i].v[1]-n*EPS]);
            tris[i] = Triangle([tris[i].v[0]+n*EPS,tris[i].v[1]+n*EPS,tris[i].v[2]+n*EPS]);
            shaders[i] = materialIdx;
        }

        //xxx
        TriangleTex[] tex = new TriangleTex[tris.length];

        mFaces ~= tris;
        mFaces ~= trisInv;
        mFaceTex ~= tex;
        mFaceTex ~= tex;
        mShaders ~= shaders;
        mShaders ~= shaders;
    }
}
